﻿using System;
using System.Text;
using System.Collections.Generic;
using EnvDTE;
using EnvDTE80;
using System.Windows.Forms;

namespace CSharpCodeAssistant
{
    class CodeGenerator
    {
        DTE2 appObject;
        CodeClass2 codeClass;
        CodeStruct2 codeStruct;

        internal CodeGenerator(DTE2 appObject)
        {
            this.appObject = appObject;            
        }


        internal CodeClass2 CodeClass
        {
            get
            {
                return codeClass;
            }
        }

        internal CodeStruct2 CodeStruct
        {
            get
            {
                return codeStruct;
            }
        }

        /// <summary>
        /// This method is used for debugging pupose and may be deleted later on.
        /// </summary>
        internal void TestTextSelectionCodeElement()
        {           
            if (codeClass != null || codeStruct != null)
            {
                ShowSelectOptionForm();
                //CheckIfPropertyExists(null);
            }
        }

        /// <summary>
        /// This method is used to get the current cursor point and identify if the point falls in a class or a struct.
        /// if the current cursor point does not fall under both cases then it sets both points as null.
        /// </summary>
        internal void SetCodeElement4CurrentPoint()
        {

            EditPoint editPoint = GetEditPoint();
            if (editPoint != null)
            {
                try
                {
                    codeClass = editPoint.CodeElement[vsCMElement.vsCMElementClass] as CodeClass2;
                }
                catch (Exception e)
                {
                    //Exception means that user placed his cursor at point which cannot be associated with a class.
                    //Note: user can place his cursor in a struct so check for that.
                    try
                    {
                        codeStruct = editPoint.CodeElement[vsCMElement.vsCMElementStruct] as CodeStruct2;
                    }
                    catch (Exception e1)
                    {
                        //Exception means that user placed his cursor at a point which cannot be associated with neither a class nor a stuct.
                        //so show a message box and return back.
                        MessageBox.Show(Constant.MSG_NOT_IN_BOUNDARY);
                        //return back
                        return;
                    }
                }
            }
        }

        private EditPoint2 GetEditPoint()
        {
            TextSelection ts = getTextSelection();
            if (ts != null)
            {
                //now get the edit point to get the codeClass or codeStruct.
                EditPoint2 editPoint = ts.TopPoint.CreateEditPoint() as EditPoint2;
                return editPoint;
            }
            return null;
        }

        /// <summary>
        /// This method is used to add semi colon at the end of the line.
        /// </summary>
        internal void AddSemiColon()
        {
            EditPoint2 ep = GetEditPoint();            
            if (ep != null)
            {                           
                string lineString=ep.GetLines(ep.Line, ep.Line + 1);
                if (lineString!=null && lineString!=""&& !lineString.EndsWith(";"))
                {
                    ep.EndOfLine();
                    ep.Insert(";");                    
                }
            }
        }

        /// <summary>
        /// This method is used to add a new line and move the cursor to the newly created line.
        /// </summary>
        internal void AddNewLine()
        {           
            TextDocument document = appObject.ActiveDocument.Object("TextDocument") as TextDocument;
            if(document!=null&& document.Selection!=null)
            {
                document.Selection.EndOfLine();
                document.Selection.NewLine(1);
            }
        }

        private TextSelection getTextSelection()
        {
            //first get the text selection for the current point.
            TextSelection ts = null;
            if (appObject != null && appObject.ActiveDocument != null && appObject.ActiveDocument.Selection != null)
            {
                ts = appObject.ActiveDocument.Selection as TextSelection;
            }
            return ts;
        }

        /// <summary>
        /// This Method is used to get the list of variables which can be accessed from codeClass or codeStruct varibles.
        /// codeClass and codeStruct are internal members of this class.
        /// if both are null then it returns null.
        /// you have to check for nulls.
        /// </summary>
        internal List<CodeVariable2> GetListOfVariables()
        {
            return GetListOfVariables(codeClass, codeStruct);
        }

        /// <summary>
        /// This Method is used to get the list of variables which can be accessed from codeClass or codeStruct varible.
        /// if both are null then it returns null.
        /// you have to check for nulls.
        /// </summary>
        /// <param name="codeClass"></param>
        /// <param name="codeStruct"></param>
        /// <returns></returns>
        internal static List<CodeVariable2> GetListOfVariables(CodeClass2 codeClass, CodeStruct2 codeStruct)
        {
            CodeElements codeElements = null;
            List<CodeVariable2> variables = null;
            if (codeClass != null)
            {
                codeElements = codeClass.Children;

            }
            else if (codeStruct != null)
            {
                codeElements = codeStruct.Children;
            }
            else
            {
                //both are null so return empty list
                return variables;
            }
            //initialize the list.
            variables = new List<CodeVariable2>();
            foreach (CodeElement2 tempElement in codeElements)
            {
                if (tempElement != null && tempElement.Kind == vsCMElement.vsCMElementVariable)
                {
                    CodeVariable2 tempVariable = tempElement as CodeVariable2;
                    variables.Add(tempVariable);

                }
            }
            return variables;
        }

        /// <summary>
        /// This Method is used to get the list of base classes if they exist and if the user's cursor is in codeClass boundary.
        /// it returns a null if conditions violate so check for nulls.
        /// no structs because structs are sealed classes.
        /// </summary>
        /// <returns></returns>
        internal List<CodeClass2> GetListOfBaseClasses()
        {
            List<CodeClass2> bases = null;
            if (codeClass != null)
            {
                CodeElements tempBases = codeClass.Bases;
                Queue<CodeElements> tempBasesList = new Queue<CodeElements>();
                if (tempBases != null && tempBases.Count > 0)
                {
                    bases = new List<CodeClass2>();
                    tempBasesList.Enqueue(tempBases);
                }
                while (tempBasesList.Count != 0)
                {
                    tempBases = tempBasesList.Dequeue();
                    foreach (CodeElement tempBase in tempBases)
                    {
                        try
                        {
                            CodeClass2 temp = tempBase as CodeClass2;
                            if (temp != null && temp.Name != "Object")
                            {
                                bases.Add(temp);
                                if (temp.Bases != null && temp.Bases.Count > 0)
                                {
                                    tempBasesList.Enqueue(temp.Bases);
                                }
                            }
                        }
                        catch (Exception e)
                        {

                        }
                    }
                }
            }
            return bases;
        }

        /// <summary>
        /// This Method is used to get the list of non static and non private constructors which can be accessed 
        /// from codeClass or codeStruct varibles.
        /// codeClass and codeStruct are internal members of this class.
        /// if both are null then it returns null.
        /// you have to check for nulls.
        /// </summary>
        internal List<CodeFunction2> GetListOfNonStaticConstructors()
        {
            return GetListOfNonStaticConstructors(codeClass, codeStruct);
        }

        /// <summary>
        /// This Method is used to get the list of non static and non private constructors which can be accessed 
        /// from codeClass or codeStruct varible.
        /// if both are null then it returns null.
        /// you have to check for nulls.
        /// </summary>
        /// <param name="codeClass"></param>
        /// <param name="codeStruct"></param>
        /// <returns></returns>
        internal static List<CodeFunction2> GetListOfNonStaticConstructors(CodeClass2 codeClass, CodeStruct2 codeStruct)
        {
            return GetListOfNonStaticConstructors(codeClass, codeStruct, false);
        }

        /// <summary>
        /// This Method is used to get the list of non static constructors which can be accessed 
        /// from codeClass or codeStruct varible.
        /// if both are null then it returns null.
        /// you have to check for nulls.
        /// </summary>
        /// <param name="codeClass"></param>
        /// <param name="codeStruct"></param>
        /// <param name="isPrivateRequired">This flag helps to get the private constructors if set to true.</param>
        /// <returns></returns>
        internal static List<CodeFunction2> GetListOfNonStaticConstructors(CodeClass2 codeClass, CodeStruct2 codeStruct,bool isPrivateRequired)
        {
            CodeElements codeElements = null;
            List<CodeFunction2> constructors = null;
            if (codeClass != null)
            {
                try
                {
                    codeElements = codeClass.Children;
                }
                catch (Exception e)
                {
                    return constructors;
                }

            }
            else if (codeStruct != null)
            {
                try
                {
                    codeElements = codeStruct.Children;
                }
                catch (Exception e)
                {
                    return constructors;
                }
            }
            else
            {
                //both are null so return empty list
                return constructors;
            }
            //initialize the list.
            constructors = new List<CodeFunction2>();
            foreach (CodeElement2 tempElement in codeElements)
            {
                if (tempElement != null && tempElement.Kind == vsCMElement.vsCMElementFunction)
                {
                    CodeFunction2 tempConstructor = tempElement as CodeFunction2;
                    if (isPrivateRequired)
                    {
                        if (tempConstructor.FunctionKind == vsCMFunction.vsCMFunctionConstructor && !tempConstructor.IsShared)
                        {
                            constructors.Add(tempConstructor);
                        }
                    }
                    else
                    {
                        if (tempConstructor.FunctionKind == vsCMFunction.vsCMFunctionConstructor && !tempConstructor.IsShared && tempConstructor.Access != vsCMAccess.vsCMAccessPrivate && tempConstructor.Access != vsCMAccess.vsCMAccessDefault)
                        {
                            constructors.Add(tempConstructor);
                        }
                    }
                }
            }
            return constructors;
        }

        /// <summary>
        /// This method is used to the list of properties in the current selection.
        /// </summary>
        /// <returns></returns>
        internal List<CodeProperty2> GetListOfCodeProperties()
        {
            return GetListOfCodeProperties(codeClass, codeStruct);
        }

        /// <summary>
        /// This method is used to get the list of properties in from the codeClass or codeStruct variable.
        /// If both are null it return null.
        /// </summary>
        /// <param name="codeClass"></param>
        /// <param name="codeStruct"></param>
        /// <returns></returns>
        internal static List<CodeProperty2> GetListOfCodeProperties(CodeClass2 codeClass, CodeStruct2 codeStruct)
        {
            CodeElements codeProperties = null;
            List<CodeProperty2> properties = null;
            if (codeClass != null)
            {
                codeProperties = codeClass.Children;

            }
            else if (codeStruct != null)
            {
                codeProperties = codeStruct.Children;
            }
            else
            {
                //both are null so return empty list
                return properties;
            }
            //initialize the list.
            properties = new List<CodeProperty2>();
            foreach (CodeElement2 tempElement in codeProperties)
            {
                if (tempElement != null && tempElement.Kind == vsCMElement.vsCMElementProperty)
                {
                    CodeProperty2 tempProperty = tempElement as CodeProperty2;
                    properties.Add(tempProperty);

                }
            }
            return properties;
        }

        /// <summary>
        /// This method is used to get the type name of a codeVariable.
        /// </summary>
        /// <param name="codeVariable"></param>
        /// <returns></returns>
        internal static string GetTypeNameForCodeVariable(CodeVariable2 codeVariable)
        {
            string typeName = "";
            CodeTypeRef type = codeVariable.Type;
            typeName = type.AsString;
            typeName = typeName.Contains(".") ? typeName.Substring(typeName.LastIndexOf(".") + 1) : typeName;
            return typeName;
        }


        /// <summary>
        /// This method is used to get the type name of a codeParameter.
        /// </summary>
        /// <param name="codeParameter"></param>
        /// <returns></returns>
        internal static string GetTypeNameForCodeParameter(CodeParameter2 codeParameter)
        {
            string typeName = "";
            CodeTypeRef type = codeParameter.Type;
            typeName = type.AsString;
            typeName = typeName.Contains(".") ? typeName.Substring(typeName.LastIndexOf(".") + 1) : typeName;
            return typeName;
        }

        /// <summary>
        /// This Method is used to get the name of the code class or 
        /// code struct or null based on cursor position.
        /// </summary>
        /// <returns></returns>
        internal string GetThisName()
        {
            if (codeClass != null)
            {
                return codeClass.Name;
            }
            else if (codeStruct != null)
            {
                return codeStruct.Name;
            }
            else
            {
                return "";
            }
        }

        /// <summary>
        /// This method is used to show the first options window.
        /// </summary>
        internal void ShowSelectOptionForm()
        {
            if ((codeClass == null) && (codeStruct == null))
            {
                MessageBox.Show(Constant.MSG_NOT_IN_BOUNDARY);
                return;
            }

            SelectOptionsForm sof = new SelectOptionsForm(this);
            sof.ShowDialog();
        }

        /// <summary>
        /// This method is used to add constructor based on the user selection.
        /// </summary>
        /// <param name="variables"></param>
        /// <param name="constructor"></param>
        /// <param name="positionOfConstructor">This Parameter is used to fit in the constructor based on user selection.</param>
        /// <param name="access"></param>
        internal void AddConstructor(List<CodeVariable2> variables, CodeFunction2 constructor, int positionOfConstructor, string access)
        {
            EditPoint2 editPoint = GetEditPoint();
            if (editPoint != null)
            {
                StringBuilder builder = new StringBuilder(GetConstructorDefinition(variables, constructor,positionOfConstructor, access,true));
                if (constructor != null)
                {
                    string constructorDef = GetConstructorDefinition(constructor,false,false,true);
                    string constructorParamDef = constructorDef.Substring(constructorDef.IndexOf("("));
                    builder.Append(" : base" + constructorParamDef);
                }
                builder.Append("\n" + "{");
                if (variables != null)
                {
                    foreach (CodeVariable2 variable in variables)
                    {                        
                        builder.Append("\nthis."+variable.Name+" = "+variable.Name+";");
                    }
                }
                builder.Append("\n" + "}");
                editPoint.Insert(builder.ToString());
                formatDocument();

                
            }
        }

       /// <summary>
        ///  This method is used to get constructor definition based on the user selection.
       /// </summary>
       /// <param name="variables"></param>
       /// <param name="constructor"></param>
       /// <param name="positionOfConstructor"></param>
       /// <param name="access"></param>
       /// <param name="isAccessRequired"></param>
       /// <returns></returns>
        internal string GetConstructorDefinition(List<CodeVariable2> variables, CodeFunction2 constructor, int positionOfConstructor, string access,bool isAccessRequired)
        {
            StringBuilder builder = new StringBuilder("");
            if (isAccessRequired)
            {
                builder.Append(access + " ");
            }
           builder.Append(GetThisName() + "(");
            if (constructor == null)
            {
                //decrease the positionOfConstructor by 1 since no constructor.
                positionOfConstructor--;
                if (positionOfConstructor < 0)
                    positionOfConstructor = 0;
            }
            if (variables != null && variables.Count > 0)
            {
                for (int i = 0; i < positionOfConstructor; i++)
                {
                    CodeVariable2 variable = variables[i];
                    builder.Append(GetTypeNameForCodeVariable(variable) + " " + variable.Name);
                    if (i < variables.Count - 1)
                    {
                        builder.Append(", ");
                    }

                }
            }
            if (constructor != null && constructor.Parameters != null && constructor.Parameters.Count > 0)
            {
                if (variables != null && variables.Count > 0 && positionOfConstructor == variables.Count)
                {
                    //that means if the constructor is at the last in the selection so add a comma as it would be missing.
                    builder.Append(", ");
                }
                int size = 0;
                foreach (CodeElement2 temp in constructor.Parameters)
                {
                    size++;
                    CodeParameter2 parameter = temp as CodeParameter2;
                    builder.Append(GetTypeNameForCodeParameter(parameter) + " " + parameter.Name);
                    if (size != constructor.Parameters.Count)
                    {
                        //this means
                        builder.Append(", ");
                    }
                }
            }
            if (variables != null && variables.Count > 0)
            {
                //if the constructor is not the last element then add a comma.
                if (constructor != null && constructor.Parameters != null && constructor.Parameters.Count > 0 && positionOfConstructor < variables.Count)
                {
                    builder.Append(", ");
                }
                for (int i = positionOfConstructor; i < variables.Count; i++)
                {
                    CodeVariable2 variable = variables[i];
                    builder.Append(GetTypeNameForCodeVariable(variable) + " " + variable.Name);
                    if (i < variables.Count - 1)
                    {
                        builder.Append(", ");
                    }

                }
            }
            builder.Append(")");
            return builder.ToString();
        }

        /// <summary>
        /// This Method is used to get the string definition scheme of constructor.
        /// </summary>
        /// <param name="constructor"></param>
        /// <param name="isAccessRequired"></param>
        /// <param name="isTypeRequired"></param>
        /// <param name="isParamNameRequired"></param>
        /// <returns></returns>
        internal static string GetConstructorDefinition(CodeFunction2 constructor, bool isAccessRequired, bool isTypeRequired, bool isParamNameRequired)
        {
            StringBuilder builder = new StringBuilder("");
            if (constructor != null)
            {
                if (constructor.FunctionKind == vsCMFunction.vsCMFunctionConstructor)
                {

                    if (isAccessRequired)
                    {
                        string access = (String.Format("{0}", constructor.Access));
                        access = access.Replace("vsCMAccess", "").ToLower();
                        access = access.Replace("project", "internal");
                        access = access.Replace("default", "private");
                        builder.Append(access + " ");

                    }
                    builder.Append(constructor.Name);
                    builder.Append("(");
                    int size = 0;
                    foreach (CodeElement2 temp in constructor.Parameters)
                    {
                        size++;
                        CodeParameter2 parameter = temp as CodeParameter2;
                        if (isTypeRequired)
                        {
                            builder.Append(GetTypeNameForCodeParameter(parameter));
                        }
                        if (isParamNameRequired)
                        {
                            builder.Append(" " + parameter.Name);
                        }
                        if (size != constructor.Parameters.Count)
                        {
                            builder.Append(", ");
                        }
                    }
                    builder.Append(")");
                }
            }
            //MessageBox.Show(builder.ToString());
            return builder.ToString();
        }

        /// <summary>
        /// This Method Checks If there already a constructor which has similar definition.
        /// </summary>
        /// <param name="variables"></param>
        /// <param name="constructor"></param>
        /// <param name="positionOfConstructor"></param>
        /// <returns></returns>
        internal bool CheckIfConstructorExists(List<CodeVariable2> variables, CodeFunction2 constructor, int positionOfConstructor)
        {
            bool constructorExists=false;                                      
            string aboutToAddConstructorDef = GetConstructorDefinition(variables, constructor, positionOfConstructor, "", false);               
            List<CodeFunction2> constructors = GetListOfNonStaticConstructors(codeClass, codeStruct, true);
            if (aboutToAddConstructorDef.Length != 0 && constructors != null && constructors.Count > 0)
            {                  
                foreach (CodeFunction2 tempConstructor in constructors)
                {
                    string tempConstructorDefinition = GetConstructorDefinition(tempConstructor, false, true, true);                        
                    if (tempConstructorDefinition != null && tempConstructorDefinition.Equals(aboutToAddConstructorDef))
                    {                            
                        constructorExists = true;
                        break;
                    }
                }
            }
            return constructorExists;
        }

        /// <summary>
        /// This Method is used to add the property in the class.
        /// </summary>
        /// <param name="variable"></param>
        /// <param name="access"></param>
        /// <param name="type"></param>
        internal void AddProperty(CodeVariable2 variable, string access, ListOfOptionsForFirstWindow type)
        {
            if (variable != null && type != ListOfOptionsForFirstWindow.None && type != ListOfOptionsForFirstWindow.Constructor)
            {
                EditPoint2 editPoint = GetEditPoint();
                if (editPoint != null)
                {                   
                    string propertyName = GetCamelCaseName(variable.Name);
                    propertyName = CheckIfPropertyExistsOrUpdateName(propertyName, variable.Name);
                    StringBuilder builder = new StringBuilder(access + " " + GetTypeNameForCodeVariable(variable) + " " + propertyName);
                    builder.Append("\n" + "{");
                    if (type == ListOfOptionsForFirstWindow.ReadOnlyProperty || type == ListOfOptionsForFirstWindow.ReadWriteProperty)
                    {
                        builder.Append("\n" + "get");
                        builder.Append("\n" + "{");
                        builder.Append("\n" + "return " + variable.Name + ";");
                        builder.Append("\n" + "}");
                    }
                    if (type == ListOfOptionsForFirstWindow.WriteOnlyProperty || type == ListOfOptionsForFirstWindow.ReadWriteProperty)
                    {
                        builder.Append("\n" + "set");
                        builder.Append("\n" + "{");
                        builder.Append("\n" + variable.Name + " = value;");
                        builder.Append("\n" + "}");
                    }
                    builder.Append("\n" + "}");
                    editPoint.Insert(builder.ToString());
                    formatDocument();                   
                }
            }
        }

        /// <summary>
        /// This method is used to check if a property or field with propertyName exists or not.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        internal bool CheckIfPropertyExists(string propertyName)
        {
            bool propExists = false;
            List<CodeProperty2> properties = GetListOfCodeProperties();
            if (properties != null  && properties.Count > 0)
            {
                foreach (CodeProperty2 tempProperty in properties)
                {
                    if (tempProperty.Name.Equals(propertyName))
                    {
                        propExists = true;
                        break;
                    }
                }
            }
            if (!propExists)
            {
                List<CodeVariable2> variables = GetListOfVariables();
                if (variables != null && variables.Count > 0)
                {
                    foreach (CodeVariable2 variable in variables)
                    {
                        if (variable.Name.Equals(propertyName))
                        {
                            propExists = true;
                            break;
                        }
                    }
                }
            }
            return propExists;
        }

        /// <summary>
        /// This method is used to check if property exists and takes user suggestion to 
        /// 1) rename it.
        /// 2) ignore in this case null is returned.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="variableName"></param>
        /// <returns></returns>
        internal string CheckIfPropertyExistsOrUpdateName(string propertyName,string variableName)
        {
            string newPropertyName = null;
            if (CheckIfPropertyExists(propertyName))
            {
                //ask user to 
                RenamePropertyForm rpf = new RenamePropertyForm(propertyName, variableName, this);
                rpf.ShowDialog();
                newPropertyName = rpf.PropertyName;

            }
            else
            {
                newPropertyName = propertyName;
            }
            return newPropertyName;
        }

        /// <summary>
        /// This Method is used to format the document.
        /// </summary>
        internal void formatDocument()
        {
            TextSelection textPoint = getTextSelection();
            if (textPoint != null)
            {
                textPoint.SelectAll();
                textPoint.SmartFormat();
                textPoint.SelectLine();
            }
        }

        /// <summary>
        /// This method is used to get the camel case string for a give string.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        internal string GetCamelCaseName(string name)
        {
            return name.Substring(0, 1).ToUpper() + name.Substring(1);
        }

    }

    /// <summary>
    /// This Enum deals with list of options available for the first window.
    /// </summary>
    enum ListOfOptionsForFirstWindow
    {
        None,
        Constructor,
        ReadOnlyProperty,
        WriteOnlyProperty,
        ReadWriteProperty
    }
}
